Skip to content

fix(watcher): discover forwardRef/memo-wrapped components in AST analysis#2

Merged
ryandmonk merged 1 commit into
mainfrom
fix/ast-forwardref-memo-discovery
Jun 10, 2026
Merged

fix(watcher): discover forwardRef/memo-wrapped components in AST analysis#2
ryandmonk merged 1 commit into
mainfrom
fix/ast-forwardref-memo-discovery

Conversation

@ryandmonk

Copy link
Copy Markdown
Collaborator

What

Teaches the watcher's AST component discovery (collectComponents in packages/watcher/src/ast/parseIntentFromReactAst.ts) to recognize components wrapped in React.forwardRef(...) or memo(...).

Why

collectComponents recognized function declarations and bare arrow/function-expression components (const X = () => {}), but not the React.forwardRef/memo shape:

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => ...)

This is the dominant component shape in classic shadcn/ui codebases, so those components were silently missed by the watcher's AST analysis.

How

In the VariableDeclarator visitor branch, unwrap a CallExpression initializer whose callee is forwardRef or memo — matched as either a bare identifier (forwardRef(...)) or a member expression (React.forwardRef(...)) — taking the first argument when it is an arrow or function expression, before the existing arrow/function-expression check.

  • loc still spans the full VariableDeclaration (start→end lines unchanged).
  • componentKey is computed exactly as before.
  • JSX inside the wrapped render function is still analyzed for literals/semantics.

Tests

Adds a "Wrapped Component Extraction" describe block with four cases:

  • React.forwardRef<...>((props, ref) => ...) — asserts discovery as exported with correct componentName, componentKey, loc, and that inner JSX text is extracted.
  • bare forwardRef((props, ref) => ...)
  • memo(() => ...)
  • negative case (createConfig(() => ...)) confirming unrelated call wrappers are not treated as components.

All 57 tests in the file pass; tsc --noEmit is clean.

🤖 Generated with Claude Code

…ysis

collectComponents() recognized function declarations and bare arrow/
function-expression components, but not components wrapped in
React.forwardRef(...) or memo(...) — the dominant shape in classic
shadcn/ui codebases. Such components were silently missed by the
watcher's AST analyzer.

In the VariableDeclarator visitor, unwrap a CallExpression initializer
whose callee is forwardRef or memo (bare identifier or member
expression like React.forwardRef), taking the first argument when it is
an arrow/function expression, before the existing arrow/function-
expression check. loc still spans the full VariableDeclaration and
componentKey is computed as before.

Adds vitest coverage for React.forwardRef, bare forwardRef, memo, and a
negative case confirming unrelated call wrappers are not misdetected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 10, 2026 21:07
@ryandmonk ryandmonk merged commit 96e38fa into main Jun 10, 2026
2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the watcher’s React AST component discovery (collectComponents) so it can recognize component definitions wrapped in React.forwardRef(...) and memo(...), which are common in shadcn/ui-style codebases.

Changes:

  • Extend collectComponents to unwrap forwardRef/memo call-expression initializers and treat the inner render function as the component shape.
  • Add unit tests covering React.forwardRef(...), bare forwardRef(...), memo(...), and a negative non-component wrapper case.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/watcher/src/ast/parseIntentFromReactAst.ts Adds forwardRef/memo unwrapping in variable-declarator component discovery.
packages/watcher/src/ast/tests/parseIntentFromReactAst.test.ts Adds tests validating wrapped component extraction and a negative case.

Comment on lines +258 to +276
// Unwrap React.forwardRef(...) / memo(...) wrappers so that the dominant
// shadcn/ui shape `const Button = React.forwardRef((props, ref) => ...)`
// is recognized. We look at the first argument when it is an arrow/function
// expression, then fall through to the regular check below.
let unwrapped: t.Node = init;
if (t.isCallExpression(init)) {
const callee = init.callee;
const calleeName = t.isIdentifier(callee)
? callee.name
: t.isMemberExpression(callee) && t.isIdentifier(callee.property)
? callee.property.name
: undefined;
if ((calleeName === 'forwardRef' || calleeName === 'memo') && init.arguments.length > 0) {
const arg = init.arguments[0];
if (t.isArrowFunctionExpression(arg) || t.isFunctionExpression(arg)) {
unwrapped = arg;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants